"""
Grave! Important! Важно!

Proletoj el ĉiuj landoj, unuiĝu!
Workers of the world, unite!
Пролетарии всех стран, соединяйтесь!

https://tkom.pro
"""

from graphene_permissions.mixins import AuthFilter
from graphene_django import filter as gfilter
from graphene import types, relay, List, String, Int, DateTime, Boolean, Float
from django.db.models import (ForeignKey, OneToOneField, ManyToManyField, AutoField, BigAutoField,
                              UUIDField, IntegerField, BigIntegerField, BooleanField, CharField,
                              Q, F, Max, Sum, When, Case, Subquery, OuterRef, DateTimeField,
                              JSONField)
from django.utils import timezone
from django.db.models.functions import Coalesce, Least
from django.db.models.fields.related import RelatedField
from django.db.models.fields.reverse_related import ForeignObjectRel
from django.utils.translation import gettext_lazy as _
from django.core.exceptions import FieldDoesNotExist
from django import forms
from main.models import Uzanto

import django_filters
import itertools
import uuid

from komunumoj.models import Komunumo, KomunumoTakso
from muroj.models import (MurojUzantoEnskribo, MuroEnskribo)


class IntegerFilter(django_filters.filterset.Filter):
    field_class = forms.IntegerField


def get_filter_type(field, model=None):
    """
    Возвращает тип django_filter по типу поля Django
    """
    if isinstance(field, str) and model:
        field_name = field.split('__', maxsplit=1)
        other_name = field_name[1] if len(field_name) > 1 else None
        field = model._meta.get_field(field_name[0])

        if isinstance(field, RelatedField) and other_name:
            return get_filter_type(other_name, field.related_model)

    if isinstance(field, CharField):
        filter_type = django_filters.CharFilter
    elif isinstance(field, UUIDField):
        filter_type = django_filters.UUIDFilter
    elif isinstance(field, (IntegerField, AutoField)):
        filter_type = IntegerFilter
    elif isinstance(field, Float):
        filter_type = django_filters.NumberFilter
    else:
        filter_type = None
        
    return filter_type


def gen_type_in_class(type_class):
    """
    Генерирует InFilter для заданного типа поля модели Django
    """
    filter_type = get_filter_type(type_class)

    if filter_type is None:
        return django_filters.BaseInFilter

    return type(
        '%sInFilter' % type_class.__name__,
        (django_filters.BaseInFilter, filter_type),
        {**(django_filters.BaseInFilter.__dict__), **(filter_type.__dict__)}
    )


def build_search_class(model, fields):
    """
    Возвращает вызываемый класс, который строит
    фильтр для запроса модели по заданным полям, объединяя
    предикаты оператором OR
    Используется для быстрого поиска по моделям
    """
    class_name = str('%sSearchFunction' % model.__name__)
    parent_classes = (object,)
    args = {
        # ключ - имя поля, значение - тип, к которому нужно преобразовать
        'fields': {},
    }

    def get_field_python_type(model, field):
        field_name = field.split("__")[0]
        model_field = model._meta.get_field(field_name)
        _type = None

        if isinstance(model_field, (ForeignKey, OneToOneField, ManyToManyField)):
            rel_field_name = field.replace('%s__' % field_name, '')
            rel_model = model._meta.get_field(field_name).related_model

            if rel_field_name:
                _type = get_field_python_type(rel_model, rel_field_name)
            else:
                raise TypeError(
                    '%s \'%s\' %s' % (model_field.__class__.__name__, _('A field with the'),
                                      _('type must have a subquery with an indication of the associated model field.'))
                )
        elif isinstance(model_field, (IntegerField, BigIntegerField, BigAutoField, AutoField)):
            _type = int
        elif isinstance(model_field, (JSONField, CharField)):
            _type = str
        elif isinstance(model_field, UUIDField):
            _type = uuid.UUID
        else:
            raise TypeError('%s: \'%s\'' % _('Unsupported field type'))

        return _type

    for field in fields:
        args['fields'][field] = get_field_python_type(model, field)

    def call_function(self, queryset, name, value):
        filters = Q()
        for _field, _type in getattr(self, 'fields').items():
            try:
                predicat = {_field: _type(value)}
                filters |= Q(**predicat)
            except ValueError:
                pass
        return queryset.filter(filters)

    args['__call__'] = call_function

    return type(
        class_name,
        parent_classes,
        args
    )


def get_target_field(model, name):
    field_name = name.split('__', maxsplit=1)
    other_name = field_name[1] if len(field_name) > 1 else None

    try:
        field = model._meta.get_field(field_name[0])
    except FieldDoesNotExist:
        return None

    if (isinstance(field, RelatedField) or isinstance(field, ForeignObjectRel)) and other_name:
        return get_target_field(field.related_model, other_name) or field

    return field


def extend_filter_class(filter_class, type_node):
    """
    Расширяет возможности стандартного класса FilterSet,
    сгенерированного DjangoConnectionField.
    1. Создает фильтры для внешних ключей по нативному значению,
       если поле внешнего ключа AutoField или Integer.
    2. Создает фитры по полю id, если оно существует и имеет типы
       Autofield или Integer
    """
    args = {**filter_class.base_filters}
    expressions = (None, 'gt', 'gte', 'lt', 'lte', 'in')
    spec_fields = ('forigo', 'arkivo', 'publikigo')
    spec_exists = False
    json_filter_fields = {}
    search_fields = tuple(type_node.search_fields) if 'search_fields' in dir(type_node) else None

    # Проверяем фильтры по GlobalID, подменяем на Int
    for arg, val in filter(lambda x: isinstance(x[1], gfilter.GlobalIDFilter), args.items()):
        target_field = get_target_field(filter_class._meta.model, arg)
        new_type = get_filter_type(target_field)
        args[arg] = new_type(field_name=arg) if new_type else val

    if isinstance(filter_class._meta.fields, dict):
        for field, expr in filter_class._meta.fields.items():
            fld = get_target_field(type_node._meta.model, field)

            if expr.count('in'):
                method_name = 'resolve_{}_in'.format(field)
                args['{}__in'.format(field)] = django_filters.CharFilter(method=method_name)
                def resolve(self, qs, name, value):
                    # удаляем лишние пробелы внутри
                    value = value.replace(' ', '')
                    gids = value.split(',')
                    return qs.filter(**{
                        name: gids,
                    })
                args[method_name] = resolve
               
            if expr.count('not_in'):
                args['{}__not_in'.format(field)] = gen_type_in_class(fld.__class__)(field_name=field,
                                                                                    lookup_expr='not_in')
            if expr.count('not_contains'):
                method_name = 'resolve_{}_not_contains'.format(field)
                args['{}__not_contains'.format(field)] = (
                    get_filter_type(field, type_node._meta.model)(method=method_name)
                )

                def resolve(self, qs, name, value):
                    field_name = name.replace('not_contains', 'contains')
                    return qs.filter(~Q(**{field_name: value}))

                args[method_name] = resolve

    # Подготовка для JSONField фитров
    if 'json_filter_fields' in dir(type_node):
        if type(getattr(type_node, 'json_filter_fields')) in (list, tuple):
            for cur in getattr(type_node, 'json_filter_fields'):
                cur_field_name = cur.split('__')[0]
                json_filter_fields[cur_field_name] = {
                    'name': cur.replace('%s__' % cur_field_name, ''),
                    'expr': ['contains']
                }
        elif isinstance(getattr(type_node, 'json_filter_fields'), dict):
            for cur, val in getattr(type_node, 'json_filter_fields').items():
                cur_field_name = cur.split('__')[0]
                json_filter_fields[cur_field_name] = {
                    'name': cur.replace('%s__' % cur_field_name, ''),
                    'expr': [expr for expr in val]
                }

    # Быстрый поиск по указанным полям (объеденены оператором OR)
    if search_fields:
        callable_class = build_search_class(type_node._meta.model, search_fields)()
        args['serchi'] = django_filters.CharFilter(method=callable_class)

    for field in type_node._meta.model._meta.fields:
        # Построение фильтров для FK, поля "id"
        if (isinstance(field, ForeignKey) and field.name not in json_filter_fields) \
                or (field.name == 'id' and not isinstance(field, ForeignKey)):
            model = field.remote_field.model if field.name != 'id' else type_node._meta.model
            orign_field_name = field.remote_field.field_name if field.name != 'id' else field.name
            orign_field = model._meta.get_field(orign_field_name)

            if (isinstance(orign_field, AutoField) or isinstance(orign_field, BigIntegerField) or
                    isinstance(orign_field, IntegerField) or isinstance(orign_field, BigIntegerField)):
                is_orign_id = (field.name == 'id')
                filter_field_name = '%s_id' % field.name if not is_orign_id else field.name

                for expr in expressions:
                    if expr:
                        filter_name = ('obj_' if is_orign_id else '') + '%s__%s' % (filter_field_name, expr)

                        if expr == 'in':
                            args[filter_name] = django_filters.BaseInFilter(field_name=filter_field_name, lookup_expr=expr)
                        else:
                            args[filter_name] = django_filters.NumberFilter(field_name=filter_field_name, lookup_expr=expr)
                    else:
                        filter_name = filter_field_name if not is_orign_id else 'obj_%s' % field.name
                        args[filter_name] = django_filters.NumberFilter(field_name=filter_field_name)
        elif field.name in json_filter_fields:
            # построение фильтров для JSONField
            related_field_name = json_filter_fields[field.name]['name'].replace('__enhavo', '').split('__')[-1]

            if (isinstance(field, ForeignKey)
                    and not isinstance(field.related_model._meta.get_field(related_field_name), JSONField)):
                raise TypeError(('%s \'%s\' %s' % _('The associated model field'),
                                 related_field_name, _('is not JSONField')))

            for expr in json_filter_fields[field.name]['expr']:
                filter_field_name = '%s__%s' % (field.name, json_filter_fields[field.name]['name'])
                filter_name = filter_field_name if expr == 'contains' else '%s__%s' % (filter_field_name, expr)
                args[filter_name] = django_filters.CharFilter(field_name=filter_field_name, lookup_expr=expr)

    if len(args) or not hasattr(filter_class, 'get_queryset'):
        def get_queryset(self):
            return getattr(self, 'qs')

        args['get_queryset'] = get_queryset
        args['Meta'] = type(
            str('Meta'),
            (object,),
            {
                'fields': filter_class._meta.fields,
                'model': filter_class._meta.model
            }
        )

        new_filter_class = type(
            str('%sSiriusoExtendFilterSet' % (type_node)),
            (filter_class,),
            args
        )
        return new_filter_class

    return filter_class


class SiriusoFilterConnectionField(AuthFilter):
    """
    1. Подключает проверку стандартных прав доступа Django
    2. Дополняет FilterSet условиями на служебные технические поля:
        а) forigo (признак удаления) - если запросил простой пользователь,
            то всегда forigo = False, если запросил администратор, то фильтр
            не подменяется.
        б) arkivo (признак архива) - если запросил пользователь не владелец
            объекта, то всегда arkivo = False, если владелец объекта или админ,
            то фильтр не подменяется.
        в) publikigo (признак публикации) - те же условия, что и для поля arkivo

    !!! ВНИМАНИЕ !!!
        Пункт 2 должен быть учтен при реализации прав django на объекты,
        т.е. после внедрения системы прав нужно убрать принудительные подмены
        фильтрв.
    """
    __spec_fields = None

    def __init__(self, node_type, additional_fields=None, *args, **kwargs):
        # Дополнительные поля, если таковые есть
        if additional_fields:
            if isinstance(additional_fields, dict):
                for field, val in additional_fields.items():
                    kwargs.setdefault(field, val)
            else:
                raise TypeError('\'%s\' %s' % ('additional_fields', 'must be a dictionary'))

        # Добавляем аргумент orderBy для реализации сортировки
        kwargs.setdefault('orderBy', List(of_type=String))

        # Добавляем поле enDato для возможности получения рейтинга на дату
        if node_type._meta.model in (Komunumo,):
            kwargs.setdefault('enDato', DateTime())

        super().__init__(node_type, *args, **kwargs)

    @property
    def filterset_class(self):
        fl_class = super().filterset_class
        return extend_filter_class(fl_class, self.node_type)

    @staticmethod
    def komunumo_queryset(model, queryset, **kwargs):
        en_dato = kwargs.get('enDato') or timezone.now()
        kom_dict = {
            Komunumo: {
                'condition': {
                    'muroenskribo__forigo': False,
                    'muroenskribo__publikigo': True,
                    'muroenskribo__arkivo': False,
                },
                'aldone': [Q(muroenskribo__publikiga_dato__lte=en_dato),
                           Q(muroenskribo__arkiva_dato__isnull=True) |
                           Q(muroenskribo__arkiva_dato__gte=en_dato),
                           Q(muroenskribo__foriga_dato__isnull=True) |
                           Q(muroenskribo__foriga_dato__gte=en_dato)],
                'dato': F('muroenskribo__publikiga_dato')
            },
            Uzanto: {
                'condition': {
                    'muroj_murojuzantoenskribo_posedanto__forigo': False,
                    'muroj_murojuzantoenskribo_posedanto__publikigo': True,
                    'muroj_murojuzantoenskribo_posedanto__arkivo': False,
                },
                'aldone': [Q(muroj_murojuzantoenskribo_posedanto__publikiga_dato__lte=en_dato),
                           Q(muroj_murojuzantoenskribo_posedanto__arkiva_dato__isnull=True) |
                           Q(muroj_murojuzantoenskribo_posedanto__arkiva_dato__gte=en_dato),
                           Q(muroj_murojuzantoenskribo_posedanto__foriga_dato__isnull=True) |
                           Q(muroj_murojuzantoenskribo_posedanto__foriga_dato__gte=en_dato)],
                'dato': F('muroj_murojuzantoenskribo_posedanto__publikiga_dato')
            }
        }

        if model in kom_dict:
            kom = kom_dict.get(model)

            if 'enDato' in kwargs:
                qs = queryset.filter(*kom.get('aldone'))
            else:
                qs = queryset

            if model in (Komunumo,):
                takso_query = KomunumoTakso.objects.filter(komunumo=OuterRef('pk')).order_by('-krea_dato')
                qs = (qs.annotate(rating=Least(Subquery(takso_query.values('takso')[:1]), 2, output_field=IntegerField()),
                                  aktiva_dato=Max(Coalesce(Subquery(takso_query.values('aktiva_dato')[:1]), F('krea_dato'), output_field=DateTimeField())),
                                  )
                      )
            else:
                qs = (qs.annotate(rating=Least(Sum(Case(When(**kom.get('condition'),
                                                             then=1), default=0)), 2, output_field=IntegerField()),
                                  aktiva_dato=Max(Coalesce(kom.get('dato'), F('krea_dato'))),
                                  )
                      )

            return qs

        return queryset

    @classmethod
    def connection_resolver(
            cls, resolver, connection, default_manager,
            queryset_resolver,
            max_limit, enforce_first_or_last,
            root, info, **args
        ):
        # Реализация ограничения данных по пункту 2
        filtering_args = queryset_resolver.keywords['filtering_args']

        filter_kwargs = {k: v for k, v in args.items() if k in filtering_args}
        
        filterset_class = queryset_resolver.keywords['filterset_class']
        
        if cls.__spec_fields is None:
            spec_fields_list = ('forigo', 'publikigo', 'arkivo', 'konfirmita')
            # проверяем спец поля на существование в модели
            spec_fields = tuple([field.name for field in filterset_class._meta.model._meta.fields
                                 if isinstance(field, BooleanField) and field.name in spec_fields_list])
            cls.__spec_fields = spec_fields

        user = info.context.user
        has_adm_perms = user.is_authenticated and (user.is_admin or user.is_superuser)

        # Переписываем спец. поля для пользователей без администраторских прав
        if not has_adm_perms:
            for field in cls.__spec_fields:
                if field in ('forigo', 'arkivo') and not field in args:
                    filter_kwargs[field] = False
                    args[field] = False
                elif field in ('publikigo', 'konfirmita') and field not in args:
                    filter_kwargs[field] = True
                    args[field] = True

        queryset = resolver(root, info, **args)

        if queryset is None:
            queryset = default_manager.get_queryset()

        # Проверяем наличие спец. условий для определения видимсоти объектов конкретному пользователю
        if hasattr(queryset.model, '_get_perm_cond'):
            queryset = queryset.filter(queryset.model._get_perm_cond(user))

        order_by = args.get('orderBy', None)

        # Если это Сообщества, то добавляем аннотированные поля статистики
        if filterset_class._meta.model in (Komunumo, Uzanto):
            queryset = cls.komunumo_queryset(filterset_class._meta.model, queryset, **args)

        if order_by:
            queryset = queryset.order_by(*order_by)

        qs = filterset_class(
            data=filter_kwargs,
            queryset=queryset
        )
        resolver = resolve_none # подменили на нулевой resolver - мы из него уже данные взяли
        # теперь нужна подмена данных на qs
        return super(AuthFilter, cls).connection_resolver(
            resolver,
            connection,
            qs.qs,
            queryset_resolver,
            max_limit,
            enforce_first_or_last,
            root,
            info,
            **args
            )

    @staticmethod
    def _get_resolver(*args, **kwargs):
        return None

def resolve_none(root, info, **kwargs):
    return None

class SiriusoUnionConnectionField(relay.ConnectionField):
    """
    Подключает Union Node
    Пока реализовано только для сообществ!!!
    """
    __models = None
    __komunumoj = (Komunumo,)
    __enskriboj = (MuroEnskribo, MurojUzantoEnskribo)

    def __init__(self, field, *args, **kwargs):
        # Проверяем, чтобы узел был наследником от Union
        assert issubclass(field._meta.node, types.Union), ("{}: Тип подключения должен быть потомком {}"
                                                           .format(self.__class__.__name__,
                                                                   types.Union.__name__))
        # Извлекаем модели узлов объединения
        _types = field._meta.node._meta.types
        self.__models = tuple(_type._meta.model for _type in _types)

        # Если в составе объединения только модели Сообществ
        # задаём доступные параметры
        if self.is_komunumoj() or self.is_enskriboj():
            kwargs.setdefault('enDato', DateTime())
            kwargs.setdefault('forigo', Boolean())
            kwargs.setdefault('arkivo', Boolean())
            kwargs.setdefault('publikigo', Boolean())
            kwargs.setdefault('serchi', String())

        if self.is_komunumoj():
            kwargs.setdefault('grava', Boolean())

        super().__init__(field, *args, **kwargs)

    @staticmethod
    def enskriboj_queryset(model, uzanto):
        query_dict = {
            MuroEnskribo: (
                Q(muro__posedanto__komunumoj_komunumomembro_posedanto__autoro=uzanto,
                  muro__posedanto__komunumoj_komunumomembro_posedanto__forigo=False) if uzanto.is_authenticated else Q()
            ) | Q(muro__posedanto__grava=True),
            MurojUzantoEnskribo: (
                Q(muro__posedanto__uzantoj_uzantojgekamaradoj_gekamarado__posedanto=uzanto,
                  muro__posedanto__uzantoj_uzantojgekamaradoj_gekamarado__akceptis2=True)
                | Q(muro__posedanto__uzantoj_uzantojgekamaradoj_posedanto__gekamarado=uzanto,
                    muro__posedanto__uzantoj_uzantojgekamaradoj_posedanto__akceptis1=True)
            )
        }

        if model in query_dict:
            if issubclass(model, MurojUzantoEnskribo) and not uzanto.is_authenticated:
                return model.objects.none()

            res = model.objects.filter(query_dict[model])
            if not issubclass(model, MurojUzantoEnskribo):
                res.union(model.objects.filter(muro__posedanto__grava=True))
            return res.distinct()

        return None

    def is_komunumoj(self):
        return not len(set(self.__models) - set(self.__komunumoj))

    def is_enskriboj(self):
        return not len(set(self.__models) - set(self.__enskriboj))

    def union_resolver(self, root, info, **kwargs):
        # Разрешает источник данных
        user = info.context.user
        # Условия для получения QuerySet'а
        cond_kwargs = {}
        # Комбинированные условия
        cond_args = []

        # Устанавливаем условия по умолчанию для Сообществ
        if self.is_komunumoj() or self.is_enskriboj():
            cond_kwargs = {
                'forigo': False,
                'arkivo': False,
                'publikigo': True
            }

            # Подставляем данные для поиска
            if 'serchi' in kwargs:
                if self.is_komunumoj():
                    cond_args.append(Q(nomo__enhavo__icontains=kwargs.get('serchi'))
                                     | Q(priskribo__enhavo__icontains=kwargs.get('serchi')))
                else:
                    cond_args.append(Q(teksto__enhavo__icontains=kwargs.get('serchi'))
                                     | Q(teksto__enhavo__icontains=kwargs.get('serchi')))

        # Дополнительные условия для авторизированных и не авторизированных пользователей
        if user.is_authenticated:
            if self.is_komunumoj():
                cond_kwargs['forigo'] = kwargs.get('forigo') or False
                cond_kwargs['arkivo'] = kwargs.get('arkivo') or False
                cond_kwargs['publikigo'] = kwargs.get('arkivo') or True

        resolved = None

        if self.is_komunumoj():
            if 'grava' in kwargs:
                cond_kwargs['grava'] = kwargs.get('grava')

            if not (user.is_authenticated and (user.is_superuser or user.is_admin)):
                cond_args.append(~Q(speco__kodo='informa'))

            querysets = (SiriusoFilterConnectionField
                             .komunumo_queryset(model, model.objects.filter(*cond_args, **cond_kwargs), **kwargs)
                         for model in self.__models)
            resolved = itertools.chain(*querysets)

            # Организуем сортировку для Сообществ
            resolved = sorted(resolved, key=lambda x: (x.rating, x.aktiva_dato), reverse=True)

        elif self.is_enskriboj():
            querysets = []

            for model in self.__enskriboj:
                conds = cond_args.copy()
                if not (user.is_authenticated and (user.is_superuser or user.is_admin)) \
                        and issubclass(model, MuroEnskribo):
                    conds.append(~Q(posedanto__speco__kodo='informa'))

                querysets.append(
                    SiriusoUnionConnectionField.enskriboj_queryset(model, user)
                        .filter(*conds, **cond_kwargs)
                        .order_by('-publikiga_dato')
                )
            resolved = itertools.chain(*querysets)
            resolved = sorted(resolved, key=lambda x: x.publikiga_dato, reverse=True)

        return resolved or list()

    def get_resolver(self, parent_resolver):
        resolver = self.union_resolver if parent_resolver.__class__.__name__ != 'function' else parent_resolver
        return super(SiriusoUnionConnectionField, self).get_resolver(resolver)
